﻿Imports System
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Diagnostics
Imports System.Globalization
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Imports System.Threading
Imports System.Workflow.Runtime

Namespace System.Workflow.Runtime.Hosting
    Public Class ManualWorkflowSchedulerService
        Inherits WorkflowSchedulerService
        ' Methods
        Public Sub New()
            Me.pendingScheduleRequests = New KeyedPriorityQueue([Of] Guid, CallbackInfo, DateTime)
            Me.scheduleRequests = New Dictionary([Of] Guid, WorkItem)
            Me.locker = New Object
        End Sub

        Public Sub New(ByVal useActiveTimers As Boolean)
            Me.pendingScheduleRequests = New KeyedPriorityQueue([Of] Guid, CallbackInfo, DateTime)
            Me.scheduleRequests = New Dictionary([Of] Guid, WorkItem)
            Me.locker = New Object
            If useActiveTimers Then
                Me.timerCallback = New TimerCallback(AddressOf Me.OnTimerCallback)
                AddHandler Me.pendingScheduleRequests.FirstElementChanged, New EventHandler([Of] KeyedPriorityQueueHeadChangedEventArgs([Of] CallbackInfo))(AddressOf Me.OnFirstElementChanged)
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: started with active timers")
            End If
        End Sub

        Public Sub New(ByVal parameters As NameValueCollection)
            Me.pendingScheduleRequests = New KeyedPriorityQueue([Of] Guid, CallbackInfo, DateTime)
            Me.scheduleRequests = New Dictionary([Of] Guid, WorkItem)
            Me.locker = New Object
            If (parameters Is Nothing) Then
                Throw New ArgumentNullException("parameters")
            End If
            Dim str As String
            For Each str In parameters.Keys
                Dim flag As Boolean
                If (str Is Nothing) Then
                    Throw New ArgumentException(String.Format(Thread.CurrentThread.CurrentCulture, ExecutionStringManager.UnknownConfigurationParameter, New Object() { "null" }))
                End If
                Dim str2 As String = parameters.Item(str)
                If Not str.Equals("UseActiveTimers", StringComparison.OrdinalIgnoreCase) Then
                    Throw New ArgumentException(String.Format(Thread.CurrentThread.CurrentCulture, ExecutionStringManager.UnknownConfigurationParameter, New Object() { str }))
                End If
                If Not Boolean.TryParse(str2, flag) Then
                    Throw New FormatException("UseActiveTimers")
                End If
                If flag Then
                    Me.timerCallback = New TimerCallback(AddressOf Me.OnTimerCallback)
                    AddHandler Me.pendingScheduleRequests.FirstElementChanged, New EventHandler([Of] KeyedPriorityQueueHeadChangedEventArgs([Of] CallbackInfo))(AddressOf Me.OnFirstElementChanged)
                    WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Started with active timers")
                End If
            Next
        End Sub

        Protected Friend Overrides Sub Cancel(ByVal timerId As Guid)
            If timerId.Equals(Guid.Empty) Then
                Throw New ArgumentException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.CantBeEmptyGuid, New Object() { "timerId" }))
            End If
            SyncLock Me.locker
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Cancel timer {0}", New Object() { timerId })
                Me.pendingScheduleRequests.Remove(timerId)
            End SyncLock
        End Sub

        Private Function CanRun(ByVal workflowInstanceId As Guid) As Boolean
            Dim flag As Boolean = False
            SyncLock Me.locker
                Dim guid As Guid
                flag = (Me.scheduleRequests.ContainsKey(workflowInstanceId) OrElse Me.HasExpiredTimer(workflowInstanceId, guid))
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: CanRun is {0}", New Object() { flag })
            End SyncLock
            Return flag
        End Function

        Private Function CreateTimerCallback(ByVal info As CallbackInfo) As Timer
            Dim utcNow As DateTime = DateTime.UtcNow
            Dim dueTime As TimeSpan = If((info.When > utcNow), DirectCast((info.When - utcNow), TimeSpan), TimeSpan.Zero)
            If (dueTime > ManualWorkflowSchedulerService.fiveMinutes) Then
                dueTime = ManualWorkflowSchedulerService.fiveMinutes
            End If
            Return New Timer(Me.timerCallback, info.InstanceId, dueTime, ManualWorkflowSchedulerService.infinite)
        End Function

        Private Function HasExpiredTimer(ByVal workflowInstanceId As Guid, <Out> ByRef timerId As Guid) As Boolean
            Dim match As Predicate([Of] CallbackInfo) = Nothing
            SyncLock Me.locker
                If (match Is Nothing) Then
                    match = Function (ByVal c As CallbackInfo) 
                        Return (c.InstanceId = workflowInstanceId)
                    End Function
                End If
                Dim info As CallbackInfo = Me.pendingScheduleRequests.FindByPriority(DateTime.UtcNow, match)
                If (Not info Is Nothing) Then
                    timerId = info.TimerId
                    Return True
                End If
            End SyncLock
            timerId = Guid.Empty
            Return False
        End Function

        Private Sub OnFirstElementChanged(ByVal source As Object, ByVal e As KeyedPriorityQueueHeadChangedEventArgs([Of] CallbackInfo))
            SyncLock Me.locker
                If Not Me.threadRunning Then
                    If (Not Me.callbackTimer Is Nothing) Then
                        Me.callbackTimer.Dispose
                        Me.callbackTimer = Nothing
                    End If
                    If ((Not e.NewFirstElement Is Nothing) AndAlso (MyBase.State = WorkflowRuntimeServiceState.Started)) Then
                        Me.callbackTimer = Me.CreateTimerCallback(e.NewFirstElement)
                    End If
                End If
            End SyncLock
        End Sub

        Protected Overrides Sub OnStarted()
            MyBase.OnStarted
            If (Not Me.timerCallback Is Nothing) Then
                SyncLock Me.locker
                    Dim info As CallbackInfo = Me.pendingScheduleRequests.Peek
                    If (Not info Is Nothing) Then
                        Me.callbackTimer = Me.CreateTimerCallback(info)
                    End If
                End SyncLock
            End If
            SyncLock Me.locker
                If ((Me.queueCounters Is Nothing) AndAlso (Not MyBase.Runtime.PerformanceCounterManager Is Nothing)) Then
                    Me.queueCounters = MyBase.Runtime.PerformanceCounterManager.CreateCounters(ExecutionStringManager.PerformanceCounterWorkflowsWaitingName)
                End If
            End SyncLock
        End Sub

        Private Sub OnTimerCallback(ByVal ignored As Object)
            Dim info As CallbackInfo = Nothing
            Try 
                SyncLock Me.locker
                    If MyBase.State.Equals(WorkflowRuntimeServiceState.Started) Then
                        info = Me.pendingScheduleRequests.Peek
                        If (Not info Is Nothing) Then
                            If info.IsExpired Then
                                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "Timeout occured for timer for instance {0}", New Object() { info.InstanceId })
                                Me.threadRunning = True
                                Me.pendingScheduleRequests.Dequeue
                            Else
                                Me.callbackTimer = Me.CreateTimerCallback(info)
                            End If
                        End If
                    End If
                End SyncLock
                If Me.threadRunning Then
                    info.Callback.Invoke(info.InstanceId)
                    Me.RunWorkflow(info.InstanceId)
                End If
            Catch exception As ThreadAbortException
                WorkflowTrace.Host.TraceEvent(TraceEventType.Error, 0, "Timeout for instance, {0} threw exception {1}", New Object() { If((info Is Nothing), Guid.Empty, info.InstanceId), exception.Message })
                MyBase.RaiseServicesExceptionNotHandledEvent(exception, info.InstanceId)
                Throw
            Catch exception2 As Exception
                WorkflowTrace.Host.TraceEvent(TraceEventType.Error, 0, "Timeout for instance, {0} threw exception {1}", New Object() { If((info Is Nothing), Guid.Empty, info.InstanceId), exception2.Message })
                MyBase.RaiseServicesExceptionNotHandledEvent(exception2, info.InstanceId)
            Finally
                SyncLock Me.locker
                    If Me.threadRunning Then
                        Me.threadRunning = False
                        info = Me.pendingScheduleRequests.Peek
                        If (Not info Is Nothing) Then
                            Me.callbackTimer = Me.CreateTimerCallback(info)
                        End If
                    End If
                End SyncLock
            End Try
        End Sub

        Private Function ProcessTimer(ByVal workflowInstanceId As Guid) As Boolean
            Dim flag As Boolean = False
            Dim info As CallbackInfo = Nothing
            Dim empty As Guid = Guid.Empty
            SyncLock Me.locker
                Dim guid2 As Guid
                If Me.HasExpiredTimer(workflowInstanceId, guid2) Then
                    info = Me.pendingScheduleRequests.Remove(guid2)
                End If
            End SyncLock
            Try 
                If (Not info Is Nothing) Then
                    WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Processing timer {0}", New Object() { empty })
                    info.Callback.Invoke(info.InstanceId)
                    flag = True
                End If
            Catch exception As Exception
                MyBase.RaiseServicesExceptionNotHandledEvent(exception, workflowInstanceId)
            End Try
            Return flag
        End Function

        Private Function RunOne(ByVal workflowInstanceId As Guid) As Boolean
            Dim flag As Boolean = False
            Dim item As WorkItem = Nothing
            SyncLock Me.locker
                If Me.scheduleRequests.ContainsKey(workflowInstanceId) Then
                    item = Me.scheduleRequests.Item(workflowInstanceId)
                    Me.scheduleRequests.Remove(workflowInstanceId)
                End If
            End SyncLock
            Try 
                If (item Is Nothing) Then
                    Return flag
                End If
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Executing {0}", New Object() { workflowInstanceId })
                If (Not Me.queueCounters Is Nothing) Then
                    Dim counter As PerformanceCounter
                    For Each counter In Me.queueCounters
                        counter.RawValue = Me.scheduleRequests.Count
                    Next
                End If
                item.Invoke(Me)
                flag = True
            Catch exception As Exception
                MyBase.RaiseServicesExceptionNotHandledEvent(exception, workflowInstanceId)
            End Try
            Return flag
        End Function

        Public Function RunWorkflow(ByVal workflowInstanceId As Guid) As Boolean
            If workflowInstanceId.Equals(Guid.Empty) Then
                Throw New ArgumentException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.CantBeEmptyGuid, New Object() { "workflowInstanceId" }))
            End If
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Running workflow {0}", New Object() { workflowInstanceId })
            Dim flag As Boolean = False
            Do While Me.CanRun(workflowInstanceId)
                If (Not Me.RunOne(workflowInstanceId) AndAlso Not Me.ProcessTimer(workflowInstanceId)) Then
                    Return flag
                End If
                flag = True
            Loop
            Return flag
        End Function

        Protected Friend Overrides Sub Schedule(ByVal callback As WaitCallback, ByVal workflowInstanceId As Guid)
            If (callback Is Nothing) Then
                Throw New ArgumentNullException("callback")
            End If
            If workflowInstanceId.Equals(Guid.Empty) Then
                Throw New ArgumentException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.CantBeEmptyGuid, New Object() { "workflowInstanceId" }))
            End If
            SyncLock Me.locker
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Schedule workflow {0}", New Object() { workflowInstanceId })
                If Not Me.scheduleRequests.ContainsKey(workflowInstanceId) Then
                    Me.scheduleRequests.Add(workflowInstanceId, New WorkItem(callback, workflowInstanceId))
                End If
            End SyncLock
            If (Not Me.queueCounters Is Nothing) Then
                Dim counter As PerformanceCounter
                For Each counter In Me.queueCounters
                    counter.RawValue = Me.scheduleRequests.Count
                Next
            End If
        End Sub

        Protected Friend Overrides Sub Schedule(ByVal callback As WaitCallback, ByVal workflowInstanceId As Guid, ByVal whenUtc As DateTime, ByVal timerId As Guid)
            If (callback Is Nothing) Then
                Throw New ArgumentNullException("callback")
            End If
            If workflowInstanceId.Equals(Guid.Empty) Then
                Throw New ArgumentException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.CantBeEmptyGuid, New Object() { "workflowInstanceId" }))
            End If
            If timerId.Equals(Guid.Empty) Then
                Throw New ArgumentException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.CantBeEmptyGuid, New Object() { "timerId" }))
            End If
            SyncLock Me.locker
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "ManualWorkflowSchedulerService: Schedule timer {0} for workflow {1} at {2}", New Object() { timerId, workflowInstanceId, whenUtc })
                Me.pendingScheduleRequests.Enqueue(timerId, New CallbackInfo(callback, workflowInstanceId, timerId, whenUtc), whenUtc)
            End SyncLock
        End Sub

        Protected Friend Overrides Sub [Stop]()
            MyBase.Stop
            If (Not Me.timerCallback Is Nothing) Then
                SyncLock Me.locker
                    If (Not Me.callbackTimer Is Nothing) Then
                        Me.callbackTimer.Dispose
                        Me.callbackTimer = Nothing
                    End If
                End SyncLock
            End If
        End Sub


        ' Fields
        Private callbackTimer As Timer
        Private Shared fiveMinutes As TimeSpan = New TimeSpan(0, 5, 0)
        Private Shared infinite As TimeSpan = New TimeSpan(-1)
        Private locker As Object
        Private pendingScheduleRequests As KeyedPriorityQueue([Of] Guid, CallbackInfo, DateTime)
        Private queueCounters As IList([Of] PerformanceCounter)
        Private scheduleRequests As Dictionary([Of] Guid, WorkItem)
        Private threadRunning As Boolean ModReq(IsVolatile)
        Private ReadOnly timerCallback As TimerCallback
        Private Const USE_ACTIVE_TIMERS_KEY As String = "UseActiveTimers"

        ' Nested Types
        Private Class CallbackInfo
            ' Methods
            Public Sub New(ByVal callback As WaitCallback, ByVal instanceId As Guid, ByVal timerId As Guid, ByVal [when] As DateTime)
                Me.callback = callback
                Me.when = [when]
                Me.instanceId = instanceId
                Me.timerId = timerId
            End Sub


            ' Properties
            Public ReadOnly Property Callback As WaitCallback
                Get
                    Return Me.callback
                End Get
            End Property

            Public ReadOnly Property InstanceId As Guid
                Get
                    Return Me.instanceId
                End Get
            End Property

            Public ReadOnly Property IsExpired As Boolean
                Get
                    Return (DateTime.UtcNow >= Me.when)
                End Get
            End Property

            Public ReadOnly Property TimerId As Guid
                Get
                    Return Me.timerId
                End Get
            End Property

            Public ReadOnly Property [When] As DateTime
                Get
                    Return Me.when
                End Get
            End Property


            ' Fields
            Private callback As WaitCallback
            Private instanceId As Guid
            Private timerId As Guid
            Private [when] As DateTime
        End Class
    End Class
End Namespace

